package com.athena.framework.web;

import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.SecureUtil;
import com.athena.util.RedisClient;
import com.athena.util.SerializeUtil;
import com.athena.util.SessionUtil;
import org.apache.catalina.*;
import org.apache.catalina.session.ManagerBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;

import java.io.IOException;
import java.util.Arrays;
import java.util.Set;

/**
 * @author eguid
 */
public class RedisSessionManager extends ManagerBase implements Lifecycle {

    /**
     * 日志对象
     */
    private static Logger logger = LoggerFactory.getLogger(RedisSessionManager.class);

    protected byte[] NULL_SESSION = "null".getBytes();

    protected RedisSessionHandlerValve handlerValve;
    protected ThreadLocal<RedisSession> currentSession = new ThreadLocal<RedisSession>();
    protected ThreadLocal<String> currentSessionId = new ThreadLocal<String>();
    protected ThreadLocal<Boolean> currentSessionIsPersisted = new ThreadLocal<Boolean>();

    protected static String name = "RedisSessionManager";

    private static String key = "athena:session:";

    @Override
    public int getRejectedSessions() {
        // Essentially do nothing.
        return 0;
    }

    public void setRejectedSessions(int i) {
        // Do nothing.
    }

    protected Jedis getConnection() {
        return RedisClient.getJedis();
    }

    @Override
    public void load() throws ClassNotFoundException, IOException {

    }

    @Override
    public void unload() throws IOException {

    }

    /**
     * Start this component and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @throws LifecycleException if this component detects a fatal error that prevents this
     *                            component from being used
     */
    @Override
    protected synchronized void startInternal() throws LifecycleException {
        boolean isSucc = false;
        try {
            logger.info("准备开启redis-session-Manager处理器 ... ");
            super.startInternal();
            setState(LifecycleState.STARTING);
            Boolean attachedToValve = false;
            Valve[] values = getContext().getPipeline().getValves();
            for (Valve valve : values) {
                if (valve instanceof RedisSessionHandlerValve) {
                    logger.info("初始化redis-session-Manager处理器 ... ");
                    this.handlerValve = (RedisSessionHandlerValve) valve;
                    this.handlerValve.setRedisSessionManager(this);
                    attachedToValve = true;
                    break;
                }
            }

            if (!attachedToValve) {
                String error = "重大错误：redis-session-Manager无法添加到会话处理器，session在请求后不能正常启动处理器！";
                throw new LifecycleException(error);
            }
            isSucc = true;
        } catch (Exception e) {
            throw e;
        } finally {
            if (isSucc) {
                logger.info("redis-session-manager初始化成功");
            } else {
                logger.info("redis-session-manager初始化失败");
            }
        }
    }

    /**
     * Stop this component and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
     *
     * @throws LifecycleException if this component detects a fatal error that prevents this
     *                            component from being used
     */
    @Override
    protected synchronized void stopInternal() throws LifecycleException {
        logger.info("停止redis-session-manager处理器!");
        setState(LifecycleState.STOPPING);

        super.stopInternal();
    }

    @Override
    public Session createSession(String sessionId) {
        // 初始化设置并创建一个新的session返回
        RedisSession session = (RedisSession) createEmptySession();
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);

        if (null == sessionId) {
            // 重新生成一个sessionId
            sessionId = SessionUtil.generateSessionId();
        }
        new RedisClient().setValue((key + sessionId).getBytes(),NULL_SESSION);

        session.setId(sessionId);
        session.tellNew();

        currentSession.set(session);
        currentSessionId.set(sessionId);
        currentSessionIsPersisted.set(false);

        return session;
    }

    @Override
    public Session createEmptySession() {
        return new RedisSession(this);
    }

    @Override
    public void add(Session session) {
        try {
            save(session);
        } catch (IOException e) {
            throw new RuntimeException("保存session失败", e);
        }
    }

    @Override
    public Session findSession(String id) throws IOException {
        RedisSession session = null;
        if (id == null) {
            session = null;
            currentSessionIsPersisted.set(false);
        } else if (id.equals(currentSessionId.get())) {
            session = currentSession.get();
        } else {
            session = loadSessionFromRedis(id);
            if (session != null) {
                currentSessionIsPersisted.set(true);
            }
        }
        currentSession.set(session);
        currentSessionId.set(id);
        return session;
    }

    public void clear() {

        Jedis jedis = null;
        try {
            jedis = getConnection();

            Set<byte[]> keys = jedis.keys((key + "*").getBytes());
            for (byte[] key : keys) {
                jedis.del(key);
            }
        } finally {
            RedisClient.returnJedis(jedis);
        }
    }

    public int getSize() throws IOException {

        Jedis jedis = null;
        try {
            jedis = getConnection();
            return jedis.keys((key + "*").getBytes()).size();
        } finally {
            RedisClient.returnJedis(jedis);
        }
    }

    public String[] keys() throws IOException {
        Jedis jedis = null;
        try {
            jedis = getConnection();
            Set<String> keySet = jedis.keys(key + "*");
            return keySet.toArray(new String[keySet.size()]);
        } finally {
            RedisClient.returnJedis(jedis);
        }
    }

    public RedisSession loadSessionFromRedis(String id) throws IOException {
        RedisSession session;
        Jedis jedis = getConnection();

        try{
            byte[] data = jedis.get((key + id).getBytes());
            if (data == null) {

                session = null;
            } else if (Arrays.equals(NULL_SESSION, data)) {
                throw new IllegalStateException("Race condition encountered: attempted to load session[" + id
                        + "] which has been created but not yet serialized.");
            } else {
                session = (RedisSession) SerializeUtil.unserialize(data);
                session.setId(id);
                session.setNew(false);
                session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
                session.access();
                session.setValid(true);
                session.resetDirtyTracking();
                session.setManager(this);
            }
        }finally {
            RedisClient.returnJedis(jedis);
        }
        return session;
    }

    /**
     * save session to redis
     *
     * @param session
     * @throws IOException
     */
    public void save(Session session) throws IOException {

        RedisSession redisSession = (RedisSession) session;

        boolean sessionIsDirty = redisSession.isDirty();

        redisSession.resetDirtyTracking();
        byte[] binaryId = (key + redisSession.getId()).getBytes();

        Jedis jedis = getConnection();

        if (sessionIsDirty || !currentSessionIsPersisted.get()) {
            jedis.set(binaryId, SerializeUtil.serialize(redisSession));
        }

        currentSessionIsPersisted.set(true);

        jedis.expire(binaryId, getContext().getSessionTimeout() * 60);

        RedisClient.returnJedis(jedis);

    }

    @Override
    public void remove(Session session) {
        remove(session, false);
    }

    @Override
    public void remove(Session session, boolean update) {
        if (session == null) {
            return;
        }
        Jedis jedis = getConnection();
        jedis.del(key + session.getId());

        RedisClient.returnJedis(jedis);
    }

    public void afterRequest() {
        RedisSession redisSession = currentSession.get();
        if (redisSession != null) {
            currentSession.remove();
            currentSessionId.remove();
            currentSessionIsPersisted.remove();
        }
    }

    @Override
    public void processExpires() {
        //redis的key设置了自动过期,这里我们不需要做什么处理
    }
}
